package edu.upol.paradise.runtime;

import java.io.File;
import java.io.InputStream;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Stack;
import java.util.UUID;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.log4j.Logger;

import edu.upol.paradise.compiler.CompiledBlockMetadescriptor;
import edu.upol.paradise.compiler.ParadiseCompiledBlock;
import edu.upol.paradise.compiler.ParadiseCompiledBlockStub.ExecutionStatus;
import edu.upol.paradise.compiler.ParadiseCompiler;
import edu.upol.paradise.compiler.ParadiseCompilerSettings;
import edu.upol.paradise.compiler.parser.ParadiseParser;
import edu.upol.paradise.compiler.parser.Tokenizer;
import edu.upol.paradise.compiler.types.Cons;
import edu.upol.paradise.compiler.types.JavaStaticMethodWrapper;
import edu.upol.paradise.compiler.types.JavaStaticMethodWrapper.MethodWrapper;
import edu.upol.paradise.compiler.types.Nil;
import edu.upol.paradise.compiler.types.ParadiseFloat;
import edu.upol.paradise.compiler.types.ParadiseFunction;
import edu.upol.paradise.compiler.types.ParadiseFunctor;
import edu.upol.paradise.compiler.types.ParadiseNumber;
import edu.upol.paradise.compiler.types.ParadiseObject;
import edu.upol.paradise.compiler.types.ParadisePair;
import edu.upol.paradise.compiler.types.ParadiseSymbol;
import edu.upol.paradise.compiler.types.VariablePassingWrapper;

public class ParadiseRuntime {
	static final Logger log = Logger.getLogger(ParadiseRuntime.class);
	
	public ParadiseRuntime(){
		log.debug("Creating new paradise runtime " + this);
	}
	
	private String runtimeUUID = UUID.randomUUID().toString();
	
	private ParadiseCompilerSettings compilerSettings;
	
	private ParadiseCompiler    compiler;
	private ParadiseParser      parser;
	private Tokenizer           tokenizer;
	private boolean 		    isAuthoritative = true;
	private ParadiseClassLoader classLoader;
	private boolean 			errorState = false;
	private String				errorMessage = "";
	
	public void initialize() throws Throwable {
		parser = new ParadiseParser();
		if (compilerSettings == null) 
			compilerSettings = new ParadiseCompilerSettings();
		compilerSettings.setDebug(true);
		compiler = new ParadiseCompiler(compilerSettings);
		setClassLoader(new ParadiseClassLoader());
		
		initializeGlobalEnvironment();
	}

	public void load(InputStream stream, String source) throws Throwable {
		load(IOUtils.toString(stream), source);
	}
	
	public void load(File file) throws Throwable {
		load(FileUtils.readFileToString(file), file.getAbsolutePath());
	}
	
	private Stack<ParadiseFunctionStackEntry> functionStack 
		= new Stack<ParadiseFunctionStackEntry>();
	
	public void load(String string, String source) throws Throwable {
		tokenizer = new Tokenizer(string, true);
		parser.addTokenSource(tokenizer);
		@SuppressWarnings("unchecked")
		CompiledBlockMetadescriptor entry = compiler.compileSource(this, (List<ParadiseObject>)(Object)parser.toASTTree(), source, "«finit»");
		System.out.println(executeBlock(entry, "<entry>", "<entry>", 0, null, new HashMap<String, ParadiseObject>()));
	}

	public ParadiseObject executeBlock(CompiledBlockMetadescriptor entry, String source, String call,
			int lineNo, VariablePassingWrapper varags, HashMap<String, ParadiseObject> boundVariables) throws Exception {
		if (functionStack.size() > 512)
			throw new RuntimeExceptions.MaxStackError("Maximum stack depth exceeded.");
		
		ParadiseFunctionStackEntry e = new ParadiseFunctionStackEntry();
		e.callName = call;
		e.descriptor = entry;
		e.filename = source;
		e.lineNo = lineNo;
		functionStack.add(e);
		
		ParadiseCompiledBlock block = (ParadiseCompiledBlock) entry.getCompiledBlock().newInstance();
		block.getClosureVariables().putAll(boundVariables);
		e.runBlock = block;
		
		block.setRuntime(this);
		block.initializeVariables(varags);
		block.run();
		
		functionStack.pop();
		
		if (block.getStatus() == ExecutionStatus.FAILED){
			if (!errorState)
				block.getException().printStackTrace(System.err);
			errorState = true;
			if (block.getException() instanceof RuntimeExceptions.UserErrorException)
				throw new RuntimeExceptions.UserErrorException();
			throw new RuntimeExceptions.Propagator();
		}
		
		return block.getResult();
	}
	
	public ParadiseObject runFunctor(ParadiseFunctor fnc, ParadiseObject...args) throws Exception {
		FunctionArgumentsHeader fncHeader = fnc.getHeader();
		VariablePassingWrapper wrapper = new VariablePassingWrapper();
		
		int i=0;
		for (String a : fncHeader.getArgs()){
			if (args.length == i)
				throw new RuntimeExceptions.FunctionParametersMismatchException("Wrong number of parameters to function: " + fnc);
			wrapper.addArgument(a, args[i++]);
		}
		if (fncHeader.isListArg()){
			ParadiseObject[] rest = Arrays.copyOfRange(args, i, args.length);
			wrapper.addArgument(fncHeader.getListArgName(), rest, true);
		}
		
		return fnc.run(this, wrapper);
	}
	
	public void interruptRuntime(){
		if (functionStack.size() > 0){
			functionStack.peek().runBlock.requestHalt();
		}
	}

	public ParadiseCompilerSettings getCompilerSettings() {
		return compilerSettings;
	}

	public void setCompilerSettings(ParadiseCompilerSettings compilerSettings) {
		this.compilerSettings = compilerSettings;
	}

	public String getRuntimeUUID() {
		return runtimeUUID;
	}

	public void setRuntimeUUID(String runtimeUUID) {
		this.runtimeUUID = runtimeUUID;
	}
	
	private Map<String, ParadiseObject> globalEnvironment;
	private HashMap<String, String> uuidToName;
	private HashMap<String, CompiledBlockMetadescriptor> uuidToFunction;

	public ParadiseObject findSymbolValue(ParadiseSymbol symbol) throws Exception {
		ParadiseObject value = globalEnvironment.get(symbol.toString());
		if (value == null)
			throw new RuntimeExceptions.SymbolUnboundException(symbol.toString());
		return value;
	}
	
	private void initializeGlobalEnvironment() throws Throwable {
		globalEnvironment = new HashMap<String, ParadiseObject>();
		uuidToName = new HashMap<String, String>();
		uuidToFunction = new HashMap<String, CompiledBlockMetadescriptor>();
		
		// System
		globalEnvironment.put("#:LOAD-COMPILED-FUNCTION", new JavaStaticMethodWrapper(new MethodWrapper(){

			public ParadiseObject run(ParadiseRuntime runtime,
					VariablePassingWrapper arguments) throws Exception {
				ParadiseObject[] args = arguments.asBaseObjectArray();
				ParadiseFunction functor = new ParadiseFunction();
				String functionUid = args[0].toString();
				ParadiseObject methodDesc = args[1];
				for (int i=2; i<args.length; i+=2){
					ParadiseSymbol boundSymbol = (ParadiseSymbol)args[i];
					ParadiseObject boundValue = args[i+1];
					functor.getBoundVariableValues().put(boundSymbol.toString(), boundValue);
				}
				functor.setHeader(FunctionArgumentsHeader.parse(methodDesc));
				functor.setUuid(functionUid);
				functor.setName(runtime.uuidToName.get(functionUid));
				return functor;
			}

			public String name() {
				return "#:<system-java-bound-method '#:LOAD-COMPILED-FUNCTION' at 0x'" + Integer.toHexString(hashCode()) + ">";
			}

			public String args() {
				return FunctionArgumentsHeader.ANY;
			}
			
		}));
		
		// constants
		globalEnvironment.put("NIL", Nil.NIL);
		globalEnvironment.put("T", ParadiseSymbol.T);
		globalEnvironment.put("*PI*", new ParadiseFloat(Math.PI));
		globalEnvironment.put("*E*", new ParadiseFloat(Math.E));
		
		// Add math functions
		globalEnvironment.put(RuntimeFunctions.ADD, new JavaStaticMethodWrapper(new MethodWrapper(){

			public ParadiseObject run(ParadiseRuntime runtime,
					VariablePassingWrapper arguments) throws Exception {
				ParadiseObject[] args = arguments.asBaseObjectArray();
				return ParadiseNumber.add(args);
			}

			public String name() {
				return "#:<java-bound-method '+' at 0x" + Integer.toHexString(hashCode()) + ">";
			}

			public String args() {
				return FunctionArgumentsHeader.ANY;
			}
			
		}));
		globalEnvironment.put(RuntimeFunctions.SUB, new JavaStaticMethodWrapper(new MethodWrapper(){

			public ParadiseObject run(ParadiseRuntime runtime,
					VariablePassingWrapper arguments) throws Exception {
				ParadiseObject[] args = arguments.asBaseObjectArray();
				return ParadiseNumber.sub(args);
			}

			public String name() {
				return "#:<java-bound-method '-' at 0x" + Integer.toHexString(hashCode()) + ">";
			}

			public String args() {
				return FunctionArgumentsHeader.ANY;
			}
			
		}));
		globalEnvironment.put(RuntimeFunctions.MUL, new JavaStaticMethodWrapper(new MethodWrapper(){

			public ParadiseObject run(ParadiseRuntime runtime,
					VariablePassingWrapper arguments) throws Exception {
				ParadiseObject[] args = arguments.asBaseObjectArray();
				return ParadiseNumber.mul(args);
			}

			public String name() {
				return "#:<java-bound-method '*' at 0x" + Integer.toHexString(hashCode()) + ">";
			}

			public String args() {
				return FunctionArgumentsHeader.ANY;
			}
			
		}));
		globalEnvironment.put(RuntimeFunctions.DIV, new JavaStaticMethodWrapper(new MethodWrapper(){

			public ParadiseObject run(ParadiseRuntime runtime,
					VariablePassingWrapper arguments) throws Exception {
				ParadiseObject[] args = arguments.getRaw("args");
				ParadiseObject first = arguments.getSingleObject("arg1");
				return ParadiseNumber.div((ParadiseNumber) first, args);
			}

			public String name() {
				return "#:<java-bound-method '/' at 0x" + Integer.toHexString(hashCode()) + ">";
			}

			public String args() {
				return "arg1 . args";
			}
			
		}));
		globalEnvironment.put(RuntimeFunctions.SQRT, new JavaStaticMethodWrapper(new MethodWrapper(){

			public ParadiseObject run(ParadiseRuntime runtime,
					VariablePassingWrapper arguments) throws Exception {
				ParadiseObject data = arguments.getSingleObject("arg");
				return ParadiseNumber.sqrt((ParadiseNumber) data);
			}

			public String name() {
				return "#:<java-bound-method 'sqrt' at 0x" + Integer.toHexString(hashCode()) + ">";
			}

			public String args() {
				return "arg";
			}
			
		}));
		
		// Conses
		globalEnvironment.put(RuntimeFunctions.CONS, new JavaStaticMethodWrapper(new MethodWrapper(){

			public ParadiseObject run(ParadiseRuntime runtime,
					VariablePassingWrapper arguments) throws Exception {
				Cons cons = new Cons();
				cons.setCar(arguments.getSingleObject("car"));
				cons.setCdr(arguments.getSingleObject("cdr"));
				return cons;
			}

			public String name() {
				return "#:<java-bound-method 'cons' at 0x" + Integer.toHexString(hashCode()) + ">";
			}

			public String args() {
				return "car cdr";
			}
			
		}));
		globalEnvironment.put(RuntimeFunctions.CAR, new JavaStaticMethodWrapper(new MethodWrapper(){

			public ParadiseObject run(ParadiseRuntime runtime,
					VariablePassingWrapper arguments) throws Exception {
				return ((ParadisePair) arguments.getSingleObject("pair")).car();
			}

			public String name() {
				return "#:<java-bound-method 'car' at 0x" + Integer.toHexString(hashCode()) + ">";
			}

			public String args() {
				return "pair";
			}
			
		}));
		globalEnvironment.put(RuntimeFunctions.CDR, new JavaStaticMethodWrapper(new MethodWrapper(){

			public ParadiseObject run(ParadiseRuntime runtime,
					VariablePassingWrapper arguments) throws Exception {
				return ((ParadisePair) arguments.getSingleObject("pair")).cdr();
			}

			public String name() {
				return "#:<java-bound-method 'cdr' at 0x" + Integer.toHexString(hashCode()) + ">";
			}

			public String args() {
				return "pair";
			}
			
		}));
		globalEnvironment.put(RuntimeFunctions.LISTP, new JavaStaticMethodWrapper(new MethodWrapper(){

			public ParadiseObject run(ParadiseRuntime runtime,
					VariablePassingWrapper arguments) throws Exception {
				return arguments.getSingleObject("list") instanceof ParadisePair ?
						ParadiseSymbol.T : Nil.NIL;
			}

			public String name() {
				return "#:<java-bound-method 'list?' at 0x" + Integer.toHexString(hashCode()) + ">";
			}

			public String args() {
				return "list";
			}
			
		}));
		
		boolean debug = getCompilerSettings().isDebug();
		getCompilerSettings().setDebug(false);
		loadBootstrap("bootstrap.system.def", "system");
		loadBootstrap("bootstrap.math.def", "math");
		loadBootstrap("bootstrap.list.def", "list");
		getCompilerSettings().setDebug(debug);
	}
	
	public void loadBootstrap(String bootstrap, String bootStrapType) throws Throwable {
		String string = IOUtils.toString(getClass().getResourceAsStream(bootstrap));
		tokenizer = new Tokenizer(string, false);
		parser.addTokenSource(tokenizer);
		@SuppressWarnings("unchecked")
		CompiledBlockMetadescriptor entry = compiler.compileSource(this, 
				(List<ParadiseObject>)(Object)parser.toASTTree(), "«bootstrap-" + bootStrapType + "»", "«finit»");
		executeBlock(entry, "<entry>", "<entry>", 0, null, new HashMap<String, ParadiseObject>());
	}

	public boolean isAuthoritative() {
		return isAuthoritative;
	}

	public void setAuthoritative(boolean isAuthoritative) {
		this.isAuthoritative = isAuthoritative;
	}

	public synchronized String addCompiledFunction(CompiledBlockMetadescriptor cfnc, String uuid){
		String name = "#:<Anonymous function at 0x" + Integer.toHexString(cfnc.hashCode()) + ">";
		uuidToName.put(uuid, name);
		uuidToFunction.put(uuid, cfnc);
		return uuid;
	}

	public CompiledBlockMetadescriptor findCompiledFunction(String uuid) {
		return uuidToFunction.get(uuid);
	}

	public ParadiseClassLoader getClassLoader() {
		return classLoader;
	}

	public void setClassLoader(ParadiseClassLoader classLoader) {
		this.classLoader = classLoader;
	}

	public void set(ParadiseSymbol symbol, ParadiseObject value) {
		if (value instanceof ParadiseFunction){
			String newName = "#:<Paradise function " + symbol.toString() + 
					" at 0x" + Integer.toHexString(uuidToFunction.get(((ParadiseFunction) value).getUuid()).getCompiledBlock().hashCode()) + ">";
			((ParadiseFunction)value).setName(newName);
			uuidToName.remove(((ParadiseFunction) value).getUuid());
			uuidToName.put(((ParadiseFunction) value).getUuid(), newName);
		}
		if (globalEnvironment.containsKey(symbol.toString()))
			globalEnvironment.remove(symbol.toString());
		globalEnvironment.put(symbol.toString(), value);
	}

	public String getErrorMessage() {
		return errorMessage;
	}

	public void setErrorMessage(String errorMessage) {
		this.errorMessage = errorMessage;
	}
}
